﻿/*
 * Virtual groups - Classes and interfaces needed to implement virtual groups
 *
 * Author: Phillip Piper
 * Date: 28/08/2009 11:10am
 *
 * Change log:
 * 2011-02-21   JPP  - Correctly honor group comparer and collapsible groups settings
 * v2.3
 * 2009-08-28   JPP  - Initial version
 *
 * To do:
 *
 * Copyright (C) 2009-2014 Phillip Piper
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
 */

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using Fluent.Lists;

namespace Fluent {
	/// <summary>
	/// A IVirtualGroups is the interface that a virtual list must implement to support virtual groups
	/// </summary>
	public interface IVirtualGroups {
		/// <summary>
		/// Return the list of groups that should be shown according to the given parameters
		/// </summary>
		/// <param name="parameters"></param>
		/// <returns></returns>
		IList<OLVGroup> GetGroups(GroupingParameters parameters);

		/// <summary>
		/// Return the index of the item that appears at the given position within the given group.
		/// </summary>
		/// <param name="group"></param>
		/// <param name="indexWithinGroup"></param>
		/// <returns></returns>
		int GetGroupMember(OLVGroup group, int indexWithinGroup);

		/// <summary>
		/// Return the index of the group to which the given item belongs
		/// </summary>
		/// <param name="itemIndex"></param>
		/// <returns></returns>
		int GetGroup(int itemIndex);

		/// <summary>
		/// Return the index at which the given item is shown in the given group
		/// </summary>
		/// <param name="group"></param>
		/// <param name="itemIndex"></param>
		/// <returns></returns>
		int GetIndexWithinGroup(OLVGroup group, int itemIndex);

		/// <summary>
		/// A hint that the given range of items are going to be required
		/// </summary>
		/// <param name="fromGroupIndex"></param>
		/// <param name="fromIndex"></param>
		/// <param name="toGroupIndex"></param>
		/// <param name="toIndex"></param>
		void CacheHint(int fromGroupIndex, int fromIndex, int toGroupIndex, int toIndex);
	}

	/// <summary>
	/// This is a safe, do nothing implementation of a grouping strategy
	/// </summary>
	public class AbstractVirtualGroups : IVirtualGroups {
		/// <summary>
		/// Return the list of groups that should be shown according to the given parameters
		/// </summary>
		/// <param name="parameters"></param>
		/// <returns></returns>
		public virtual IList<OLVGroup> GetGroups(GroupingParameters parameters) {
			return new List<OLVGroup>();
		}

		/// <summary>
		/// Return the index of the item that appears at the given position within the given group.
		/// </summary>
		/// <param name="group"></param>
		/// <param name="indexWithinGroup"></param>
		/// <returns></returns>
		public virtual int GetGroupMember(OLVGroup group, int indexWithinGroup) {
			return -1;
		}

		/// <summary>
		/// Return the index of the group to which the given item belongs
		/// </summary>
		/// <param name="itemIndex"></param>
		/// <returns></returns>
		public virtual int GetGroup(int itemIndex) {
			return -1;
		}

		/// <summary>
		/// Return the index at which the given item is shown in the given group
		/// </summary>
		/// <param name="group"></param>
		/// <param name="itemIndex"></param>
		/// <returns></returns>
		public virtual int GetIndexWithinGroup(OLVGroup group, int itemIndex) {
			return -1;
		}

		/// <summary>
		/// A hint that the given range of items are going to be required
		/// </summary>
		/// <param name="fromGroupIndex"></param>
		/// <param name="fromIndex"></param>
		/// <param name="toGroupIndex"></param>
		/// <param name="toIndex"></param>
		public virtual void CacheHint(int fromGroupIndex, int fromIndex, int toGroupIndex, int toIndex) {
		}
	}


	/// <summary>
	/// Provides grouping functionality to a FastFluentListView
	/// </summary>
	public class FastListGroupingStrategy : AbstractVirtualGroups {
		/// <summary>
		/// Create groups for FastListView
		/// </summary>
		/// <param name="parmameters"></param>
		/// <returns></returns>
		public override IList<OLVGroup> GetGroups(GroupingParameters parmameters) {
			// There is a lot of overlap between this method and FluentListView.MakeGroups()
			// Any changes made here may need to be reflected there

			// This strategy can only be used on FastFluentListViews
			var folv = (FastListView) parmameters.ListView;

			// Separate the list view items into groups, using the group key as the descrimanent
			var objectCount = 0;
			var map = new NullableDictionary<object, List<object>>();
			foreach (var model in folv.FilteredObjects) {
				var key = parmameters.GroupByColumn.GetGroupKey(model);
				if (!map.ContainsKey(key)) {
					map[key] = new List<object>();
				}

				map[key].Add(model);
				objectCount++;
			}

			// Sort the items within each group
			// TODO: Give parameters a ModelComparer property
			var primarySortColumn = parmameters.SortItemsByPrimaryColumn ? parmameters.ListView.GetColumn(0) : parmameters.PrimarySort;
			var sorter = new ModelObjectComparer(primarySortColumn, parmameters.PrimarySortOrder,
				parmameters.SecondarySort, parmameters.SecondarySortOrder);
			foreach (var key in map.Keys) {
				map[key].Sort(sorter);
			}

			// Make a list of the required groups
			var groups = new List<OLVGroup>();
			foreach (var key in map.Keys) {
				var title = parmameters.GroupByColumn.ConvertGroupKeyToTitle(key);
				if (!string.IsNullOrEmpty(parmameters.TitleFormat)) {
					var count = map[key].Count;
					var format = count == 1 ? parmameters.TitleSingularFormat : parmameters.TitleFormat;
					try {
						title = string.Format(format, title, count);
					}
					catch (FormatException) {
						title = "Invalid group format: " + format;
					}
				}

				var lvg = new OLVGroup(title);
				lvg.Collapsible = folv.HasCollapsibleGroups;
				lvg.Key = key;
				lvg.SortValue = key as IComparable;
				lvg.Contents = map[key].ConvertAll<int>(delegate(object x) { return folv.IndexOf(x); });
				lvg.VirtualItemCount = map[key].Count;
				if (parmameters.GroupByColumn.GroupFormatter != null) {
					parmameters.GroupByColumn.GroupFormatter(lvg, parmameters);
				}

				groups.Add(lvg);
			}

			// Sort the groups
			if (parmameters.GroupByOrder != SortOrder.None) {
				groups.Sort(parmameters.GroupComparer ?? new OLVGroupComparer(parmameters.GroupByOrder));
			}

			// Build an array that remembers which group each item belongs to.
			indexToGroupMap = new List<int>(objectCount);
			indexToGroupMap.AddRange(new int[objectCount]);

			for (var i = 0; i < groups.Count; i++) {
				var group = groups[i];
				var members = (List<int>) group.Contents;
				foreach (var j in members) {
					indexToGroupMap[j] = i;
				}
			}

			return groups;
		}

		private List<int> indexToGroupMap;

		/// <summary>
		/// 
		/// </summary>
		/// <param name="group"></param>
		/// <param name="indexWithinGroup"></param>
		/// <returns></returns>
		public override int GetGroupMember(OLVGroup group, int indexWithinGroup) {
			return (int) group.Contents[indexWithinGroup];
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="itemIndex"></param>
		/// <returns></returns>
		public override int GetGroup(int itemIndex) {
			return indexToGroupMap[itemIndex];
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="group"></param>
		/// <param name="itemIndex"></param>
		/// <returns></returns>
		public override int GetIndexWithinGroup(OLVGroup group, int itemIndex) {
			return group.Contents.IndexOf(itemIndex);
		}
	}


	/// <summary>
	/// This is the COM interface that a ListView must be given in order for groups in virtual lists to work.
	/// </summary>
	/// <remarks>
	/// This interface is NOT documented by MS. It was found on Greg Chapell's site. This means that there is
	/// no guarantee that it will work on future versions of Windows, nor continue to work on current ones.
	/// </remarks>
	[ComImport()]
	[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
	[Guid("44C09D56-8D3B-419D-A462-7B956B105B47")]
	internal interface IOwnerDataCallback {
		/// <summary>
		/// Not sure what this does
		/// </summary>
		/// <param name="i"></param>
		/// <param name="pt"></param>
		void GetItemPosition(int i, out NativeMethods.POINT pt);

		/// <summary>
		/// Not sure what this does
		/// </summary>
		/// <param name="t"></param>
		/// <param name="pt"></param>
		void SetItemPosition(int t, NativeMethods.POINT pt);

		/// <summary>
		/// Get the index of the item that occurs at the n'th position of the indicated group.
		/// </summary>
		/// <param name="groupIndex">Index of the group</param>
		/// <param name="n">Index within the group</param>
		/// <param name="itemIndex">Index of the item within the whole list</param>
		void GetItemInGroup(int groupIndex, int n, out int itemIndex);

		/// <summary>
		/// Get the index of the group to which the given item belongs
		/// </summary>
		/// <param name="itemIndex">Index of the item within the whole list</param>
		/// <param name="occurrenceCount">Which occurences of the item is wanted</param>
		/// <param name="groupIndex">Index of the group</param>
		void GetItemGroup(int itemIndex, int occurrenceCount, out int groupIndex);

		/// <summary>
		/// Get the number of groups that contain the given item
		/// </summary>
		/// <param name="itemIndex">Index of the item within the whole list</param>
		/// <param name="occurrenceCount">How many groups does it occur within</param>
		void GetItemGroupCount(int itemIndex, out int occurrenceCount);

		/// <summary>
		/// A hint to prepare any cache for the given range of requests
		/// </summary>
		/// <param name="i"></param>
		/// <param name="j"></param>
		void OnCacheHint(NativeMethods.LVITEMINDEX i, NativeMethods.LVITEMINDEX j);
	}

	/// <summary>
	/// A default implementation of the IOwnerDataCallback interface
	/// </summary>
	[Guid("6FC61F50-80E8-49b4-B200-3F38D3865ABD")]
	internal class OwnerDataCallbackImpl : IOwnerDataCallback {
		public OwnerDataCallbackImpl(VirtualFluentListView olv) {
			this.olv = olv;
		}

		private VirtualFluentListView olv;

		#region IOwnerDataCallback Members

		public void GetItemPosition(int i, out NativeMethods.POINT pt) {
			//System.Diagnostics.Debug.WriteLine("GetItemPosition");
			throw new NotSupportedException();
		}

		public void SetItemPosition(int t, NativeMethods.POINT pt) {
			//System.Diagnostics.Debug.WriteLine("SetItemPosition");
			throw new NotSupportedException();
		}

		public void GetItemInGroup(int groupIndex, int n, out int itemIndex) {
			//System.Diagnostics.Debug.WriteLine(String.Format("-> GetItemInGroup({0}, {1})", groupIndex, n));
			itemIndex = olv.GroupingStrategy.GetGroupMember(olv.OLVGroups[groupIndex], n);

			//System.Diagnostics.Debug.WriteLine(String.Format("<- {0}", itemIndex));
		}

		public void GetItemGroup(int itemIndex, int occurrenceCount, out int groupIndex) {
			//System.Diagnostics.Debug.WriteLine(String.Format("GetItemGroup({0}, {1})", itemIndex, occurrenceCount));
			groupIndex = olv.GroupingStrategy.GetGroup(itemIndex);

			//System.Diagnostics.Debug.WriteLine(String.Format("<- {0}", groupIndex));
		}

		public void GetItemGroupCount(int itemIndex, out int occurrenceCount) {
			//System.Diagnostics.Debug.WriteLine(String.Format("GetItemGroupCount({0})", itemIndex));
			occurrenceCount = 1;
		}

		public void OnCacheHint(NativeMethods.LVITEMINDEX from, NativeMethods.LVITEMINDEX to) {
			//System.Diagnostics.Debug.WriteLine(String.Format("OnCacheHint({0}, {1}, {2}, {3})", from.iGroup, from.iItem, to.iGroup, to.iItem));
			olv.GroupingStrategy.CacheHint(from.iGroup, from.iItem, to.iGroup, to.iItem);
		}

		#endregion
	}
}